import os
import subprocess
import sys
import zipfile
import shutil
import json
import urllib.request
import urllib.error
import stat


has_checked_sign_files = False
'''
@author: HZWei
@date: 2025-03-25
@description:  鸿蒙app安装脚本, 支持真机和模拟器, 可以像安卓一样使用adb命令安装
'''

global_exe_path = None

def hdc()->str:
    # current_dir = os.path.dirname(os.path.abspath(__file__))
    system_type = get_system_type()
    hdc_name = "hdc" if system_type == "windows" else "./hdc"
    # hdc_path = os.path.join(current_dir, hdc_name)
    try:
        if os.path.exists(hdc_name):
            os.chmod(hdc_name, stat.S_IRWXU)
    except PermissionError as e:
        print(f"无法设置hdc权限: {str(e)}")
        sys.exit(1)
    return hdc_name

def download_and_extract(url, save_dir,unzip=True):
    print(f"开始下载文件: {url}")

    file_name = get_url_filename(url)
    save_path = os.path.join(save_dir, file_name)
    # if (os.path.exists(save_path)):
    #     print(f"文件已存在: {save_path}")
    #     return save_path

    # r = subprocess.run(['curl', '-L', '-o', save_path, url], check=True)
    # if r.returncode != 0:
    #     print("下载失败")
    #     sys.exit(1)
    # print("下载完成")

    try:
        headers = {'Cache-Control': 'no-cache'}
        request = urllib.request.Request(url, headers=headers)
        with urllib.request.urlopen(request) as response:
            total_size = int(response.headers.get('Content-Length', 0))
            block_size = 8192  # 8KB 缓冲区
            downloaded_size = 0
            
            with open(save_path, 'wb') as f:
                while True:
                    buffer = response.read(block_size)
                    if not buffer:
                        break
                    downloaded_size += len(buffer)
                    f.write(buffer)
                    if total_size:
                        percent = downloaded_size * 100 / total_size
                        print(f"\r下载进度: {percent:.1f}% ({downloaded_size}/{total_size} bytes)", end='', flush=True)
            print("\n下载完成")
                
    except urllib.error.URLError as e:
        print(f"下载失败: {str(e)}")
        if os.path.exists(save_path):
            os.remove(save_path)
        sys.exit(1)
    except Exception as e:
        print(f"错误: 下载过程出错 -> {str(e)}")
        if os.path.exists(save_path):
            os.remove(save_path)
        sys.exit(1)
    if not unzip:
        return os.path.join(os.path.abspath(save_dir), file_name)
    # 解压文件
    if file_name.endswith('.zip'):
        print("开始解压文件...")
        try:
            with zipfile.ZipFile(save_path, 'r') as zip_ref:
                zip_ref.extractall(save_dir)
            print("解压完成")
#             os.remove(save_path)  # 删除原始压缩包
        except zipfile.BadZipFile:
            print(f"错误: 下载的文件不是有效的zip文件 -> {save_path}")
            sys.exit(1)
        except Exception as e:
            print(f"错误: 下载或解压过程出错 -> {str(e)}")
            if os.path.exists(save_path):
                os.remove(save_path)
            sys.exit(1)
    return os.path.abspath(save_dir)       


def get_url_filename(url: str) -> str:
    return os.path.basename(url)

def install_app(app_file_path, package_name):
     # 检查app文件是否正确
    is_exists = os.path.exists(app_file_path)
    file_ext = os.path.splitext(app_file_path)[1]
    if not is_exists:
        print("错误: 文件不存在 -> " + app_file_path)
        sys.exit(1)
    elif not file_ext:
        print("错误: 文件没有后缀名 -> " + app_file_path)
        sys.exit(1)
    elif file_ext != ".app":
        print("错误: 文件后缀名不是.app -> " + app_file_path)
        sys.exit(1)    

    # 得到app文件所在的目录
    app_file_dir =  os.path.dirname(app_file_path)
    print("file path: " + app_file_dir)
    # 解压app文件到指定目录
    output_dir = os.path.join(app_file_dir, "zip")
    os.makedirs(output_dir, exist_ok=True)

   # 清空输出目录
    for item in os.listdir(output_dir):
        item_path = os.path.join(output_dir, item)
        if os.path.isfile(item_path):
            os.remove(item_path)
        elif os.path.isdir(item_path):
            shutil.rmtree(item_path)
   
    print("开始解压文件...")
    try:
        with zipfile.ZipFile(app_file_path, 'r') as zip_ref:
            zip_ref.extractall(output_dir)
        print("解压完成")
    except zipfile.BadZipFile:
        print("错误: 不是一个有效的zip文件 -> " + app_file_path)
        sys.exit(1)
    print("解压后的文件目录: " + output_dir) 

    #  真机需要签名
    sign_hap_and_hsp(output_dir,package_name)
    
    
    # 强制停止 app进程服务
    r = subprocess.run([hdc(), "shell", "aa", "force-stop", package_name],check=True)
    print("停止app进程服务: " + r.__str__())

    # 创建临时目录
    temp_dir_name = os.urandom(16).hex()
    temp_dir_path = "/data/local/tmp/"+temp_dir_name
    print("临时目录: " + temp_dir_path)
    r = subprocess.run([hdc(), "shell", "mkdir", "-p", temp_dir_path],check=True)
    if r.returncode != 0:
        print("错误: 创建临时目录失败 -> " + r.__str__())
        sys.exit(1)
    else:
        print("创建临时目录成功")


    # 发送文件到设备 hdc file send  local_path remote_path
    # 遍历目录下的所有文件 发送到设备
    print(f"开始遍历目录: {output_dir}")
    try:
        for file in os.listdir(output_dir):
            if file.endswith('.hap') or file.endswith('.hsp'):
                local_file = os.path.join(output_dir, file)
                print(f"发送文件: {file}")
                r = subprocess.run([hdc(), "file", "send", local_file, temp_dir_path], check=True)
                if r.returncode != 0:
                    print(f"错误: 发送文件失败 -> {file}")
                    sys.exit(1)
        print("文件发送完成")
    except Exception as e:
        print(f"错误: 文件发送失败 -> {str(e)}")
        sys.exit(1)

    # 安装应用
    print("开始安装应用")
    r = subprocess.run([hdc(), "shell", "bm", "install", "-p",
     temp_dir_path], check=False,capture_output=True, text=True)
    rstr = r.stderr if r.stderr else r.stdout 
    if r.returncode!= 0:
        print("错误: 安装应用失败 -> " + rstr)    
    if rstr and "err" in rstr or "fail" in rstr or "Fail" in rstr:
        print(f"错误: 安装应用失败 -> " + rstr)
        if "9568322" in rstr:
            print(f"错误: 检查签名文件p7b中是否包含UDID, 添加后重新下载p7b文件, 替换当前的p7b文件")
            get_uuid()
        sys.exit(1)
    else:
        print("安装应用成功 -> " + rstr)   
    # 删除临时目录       
    r = subprocess.run([hdc(), "shell", "rm", "-rf", temp_dir_path])
    # 启动应用
    print("开始启动应用")
    r = subprocess.run([hdc(), "shell", "aa", "start", "-a", "EntryAbility", "-b", package_name], check=True)
    if r.returncode!= 0:
        print("错误: 启动应用失败 -> " + r.__str__())
        sys.exit(1)
    else:
        print("启动应用成功")

def get_system_type():
    if sys.platform.startswith('win'):
        return 'windows'
    elif sys.platform.startswith('darwin'):
        return 'mac'
    elif sys.platform.startswith('linux'):
        return 'linux'
    else:
        return 'unknown'

def check_hdc_exists() -> bool:
    current_dir = os.path.dirname(os.path.abspath(__file__))
    system_type = get_system_type()
    hdc_name = "hdc.exe" if system_type == "windows" else "./hdc"
    hdc_path = os.path.join(current_dir, hdc_name)

    lib_name = "libusb_shared.dll" if system_type == "windows" else "libusb_shared.dylib"
    lib_path = os.path.join(current_dir, lib_name)

    return os.path.exists(hdc_path) and os.path.exists(lib_path)

def check_file_exists(file_relative_path) -> bool:
    current_dir = os.path.dirname(os.path.abspath(__file__))
    file_all_path = os.path.join(current_dir, file_relative_path)
    return os.path.exists(file_all_path)

def getLoc():
    if not global_exe_path:
        return "./"
    else:
        return global_exe_path

def download_tools(sign_url=None): 
    if not check_file_exists("hap-sign-tool.jar"):
        url = "https://github.com/751496032/ohos_tools/raw/refs/heads/master/hap-sign-tool.zip"
        save_dir = download_and_extract(url, getLoc())
        print(f"文件保存在: {save_dir}") 
    if not check_file_exists("sign/sign-config.json") or sign_url:
        # 没有会下载测试sign文件，根据自己的项目配置自己的sign文件
        url = sign_url if sign_url else "https://gitee.com/common-apps/ohos_tools/raw/master/sign.zip"
        save_dir = download_and_extract(url, getLoc())
        print(f"文件保存在: {save_dir}") 
    if not check_hdc_exists():
        type = get_system_type()
        url = ""
        if type == "windows":
            print("windows")
            url = "https://gitee.com/common-apps/ohos_tools/raw/master/windows-tools.zip"
        elif type == "mac":
            print("mac")
            url = "https://gitee.com/common-apps/ohos_tools/raw/master/mac-tools.zip"
        save_dir = download_and_extract(url, getLoc())
        print(f"文件保存在: {save_dir}") 


# 签名
def sign_hap_and_hsp(dir_path,package_name):
    print("开始签名")
    # 遍历dir_path目录下的所有文件
    for file in os.listdir(dir_path):
        if file.endswith('.hap') or file.endswith('.hsp'):
            input_file = os.path.join(dir_path, file)
            temp_file = os.path.join(dir_path, file)
            # 获取文件名和后缀
            file_name, file_ext = os.path.splitext(file)
            # 原文件名-signed.后缀
            output_file = os.path.join(dir_path, f"{file_name}-signed{file_ext}")
           
            sign_app(input_file, output_file,package_name)
        
            # 删除临时文件
            os.remove(temp_file)
            print("签名完成")
            print("签名文件保存在: " + output_file)

def load_sign_config(package_name):
    # 读取sign-config.json文件    
    # config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sign", "sign-config.json")
    config_path = os.path.join(getLoc(),"sign/sign-config.json")
    try:
        with open(config_path, 'r', encoding='utf-8') as f:
            config_data = json.load(f)
            # 查找匹配的包名配置
            for config in config_data["configs"]:
                print("config_package_name: ", config["package_name"])
                print("download_package_name: ", package_name)
                if config["package_name"] == package_name:
                    return config
            print(f"错误: 未找到包名 {package_name} 的签名配置")
            sys.exit(1)
    except FileNotFoundError:
        print(f"错误: 签名配置文件不存在 -> {config_path}")
        sys.exit(1)
    except json.JSONDecodeError:
        print(f"错误: 签名配置文件格式错误 -> {config_path}")
        sys.exit(1)

def sign_app(input_file, output_file,package_name):
    global has_checked_sign_files
    print("开始签名 " + input_file + " -> " + output_file)

    sign_config = load_sign_config(package_name)
    app_cert_file = sign_config["app_cert_file"]
    profile_file = sign_config["profile_file"]
    keystore_file = sign_config["keystore_file"]
    key_alias = sign_config["key_alias"]
    key_pwd = sign_config["key_pwd"]
    keystore_pwd = sign_config["keystore_pwd"]

    # print(f"app_cert_file: {os.path.exists(app_cert_file)}" )
  
    # sign_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sign")
    # sign_tool = os.path.join(os.path.dirname(os.path.abspath(__file__)), "hap-sign-tool.jar")
    sign_dir = os.path.join(getLoc(),"sign")
    sign_tool = os.path.join(getLoc(),"hap-sign-tool.jar")
    sign_config_file = os.path.join(sign_dir, "sign-config.json")
  
    if not has_checked_sign_files:
        # 检查所有必需文件
        has_checked_sign_files = True
        required_files = {
            "证书文件": app_cert_file,
            "描述文件": profile_file,
            "密钥库文件": keystore_file,
            "签名工具": sign_tool,
            "签名配置文件": sign_config_file
        }
        
        for name, file_path in required_files.items():
            if not os.path.exists(file_path):
                print(f"错误: {name}不存在 -> {file_path}")
                sys.exit(1)
            print(f"{name}存在: {file_path}")

        # 检查 Java 版本 必须是17
        try:
            java_version = subprocess.check_output(["java", "-version"], stderr=subprocess.STDOUT, text=True)
            print("Java版本信息:", java_version)
        except subprocess.CalledProcessError as e:
            print("错误: 未找到Java JDK或检查失败")
            sys.exit(1)

    command = [
        "java", "-jar", sign_tool, "sign-app",
        "-keyAlias", key_alias,
        "-signAlg", "SHA256withECDSA",
        "-mode", "localSign",
        "-appCertFile", app_cert_file,
        "-profileFile", profile_file,
        "-inFile", input_file,
        "-keystoreFile", keystore_file,
        "-outFile", output_file,
        "-keyPwd", key_pwd,
        "-keystorePwd", keystore_pwd        
    ]
    result = subprocess.run(command, capture_output=True, text=True)
    if result.returncode == 0:
        print("签名成功: ", output_file)
    else:
        print("签名失败: ", input_file)
        print("错误: " + result.stderr)
        sys.exit(1)

def check_devices():
    command = [hdc(), "list", "targets"]
    result = subprocess.run(command, check=False,capture_output=True, text=True)
    if result.returncode == 0:
        out = result.stdout.strip()
        device_count = 0
        if out == "[Empty]":
            device_count = 0
        else:
            if get_system_type() == "windows":
                #  使用 findstr 统计非空行
                command = f'{hdc()} list targets | findstr /r /n ".*" | find /c ":"'
            else:
                #   wc -l
                command = f"{hdc()} list targets | wc -l"
            r = subprocess.run(command, shell=True, check=False, capture_output=True, text=True)
            try:
                device_count = int(r.stdout.strip())
            except ValueError:
                device_count = 0
        print("设备数量: ", device_count)
        if device_count == 0:
            print("错误: 没有设备连接")
            sys.exit(1)
        if device_count > 1:
            print("错误: 有多个设备连接，确保只有一个设备连接")
            sys.exit(1)  
    else:
        print("错误: " + result.stderr)
        sys.exit(1)


def get_uuid():
    command = [hdc(), "shell", "bm", "get","-u"]
    result = subprocess.run(command, capture_output=True, text=True)
    if result.returncode == 0:
        print("获取设备UUID结果: ", result.stdout)
    else:
        print("获取UUID错误, 检测是否打开“USB调试”开关: " + result.stderr)

def is_network_url(url: str) -> bool:
    return url.startswith(('http://', 'https://'))

def main():
    '''
    命令行参数: 有3个参数
    第1个参数: 包名
    第2个参数: app文件路径 可以是网络链接，也可以是本地文件路径
    第3个参数: 签名文件下载链接，可选，首次使用或更新签名时需要，有固定的格式
    python ohos-app-install.py <包名> <app文件路径> [签名文件下载链接]
    '''
    print("参数数量: " + len(sys.argv).__str__())
    if len(sys.argv) <= 2:
        print("Usage: python ohos-app-install.py <包名> <app文件路径> [签名文件下载链接]")
        sys.exit(1)
    
    package_name  = sys.argv[1]
    app_file_path = sys.argv[2]
    # package_name = "com.hzw.dsbridge"
    sign_url = None
    global global_exe_path
    if len(sys.argv) >= 4:
        sign_url = sys.argv[3]
    if sign_url:
        print("sign url: " + sign_url)    
    if len(sys.argv) >= 5:
        global_exe_path = sys.argv[4]
    if global_exe_path:
        print("exe_path: " + global_exe_path)    
    print("app file path: " + app_file_path)
    print("package name: " + package_name)
   
    download_tools(sign_url)
    check_devices()
    if is_network_url(app_file_path):
        app_file_path = download_and_extract(app_file_path, getLoc(), False)
    if app_file_path:    
        install_app(app_file_path, package_name)

   
if __name__ == "__main__":
    main()